﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Data.Odbc;
using System.Diagnostics;

namespace CITPRO.Linq.LinqToOdbc {
    public interface IOdbcReaderConvertor {
        ICollection<T> GetList<T>(OdbcDataReader reader);
        void SetRowReadHandler(Type type, Func<object, bool> handler);
        void ClearRowReadHandler(Type type);
        void ClearAllRowReadHandlers();
    }

    public class DefaultOdbcReaderConvertor : IOdbcReaderConvertor {
        private class RowReadHandler {
            public Type HandleType { get; set; }
            public Func<object, bool> Handler { get; set; }
        }

        private readonly List<RowReadHandler> rowReadHandlers = new List<RowReadHandler>();

        public ICollection<T> GetList<T>(OdbcDataReader reader) {
            var type = typeof(T);
            var result = new List<T>();
            var isAnonymous = type.Name.StartsWith("<>f__AnonymousType");
            if (isAnonymous) {
                AddAnonymousInstances(reader, type, result);
            } else {
                AddInstances(reader, result);
            }
            return result;
        }

        public void SetRowReadHandler(Type type, Func<object, bool> handler) {
            ClearRowReadHandler(type);
            rowReadHandlers.Add(new RowReadHandler {
                Handler = handler, HandleType = type
            });
        }

        public void ClearRowReadHandler(Type type) {
            var existing = rowReadHandlers.FirstOrDefault(r => r.HandleType == type);
            if (existing != null)
                rowReadHandlers.Remove(existing);
        }

        public void ClearAllRowReadHandlers() {
            rowReadHandlers.Clear();
        }


        private void AddInstances<T>(IDataReader reader, ICollection<T> result) {
            while (reader.Read()) {
                var instance = Activator.CreateInstance<T>();
                var properties = instance.GetType().GetProperties();
                var conversionErrors = new StringBuilder();
                foreach (var property in properties) {
                    var value = reader[property.Name];
                    var valueType = value.GetType();
                    try {
                        property.SetValue(instance, valueType == typeof(DBNull) ? null : value, null);
                    } catch (ArgumentException) {
                        conversionErrors.AppendFormat(
                            "Entity: {0}\nProperty:{1}\nEntity type: {2}\nValue type: {3}",
                            typeof(T).Name,
                            property.Name,
                            property.PropertyType.Name,
                            valueType.Name);
                    }

                }
                if (conversionErrors.Length > 0) {
                    throw new InvalidOperationException("Errors occurred during entity conversion:\n" + conversionErrors);
                }

                var rowReadHandler = rowReadHandlers
                    .FirstOrDefault(f => f.HandleType == typeof(T));

                if (rowReadHandler == null) {
                    result.Add(instance);
                    Debug.WriteLine("No rowhandler found. Loading to memory collection cache.");
                    continue;
                }

                if (rowReadHandler.Handler(instance)) {
                    Debug.WriteLine("Row handler allowed item.");
                    result.Add(instance);
                } else {
                    Debug.WriteLine("Row handler filtered out item.");
                }
            }
        }


        private void AddAnonymousInstances<T>(IDataReader reader, Type type, ICollection<T> result) {
            while (reader.Read()) {

                var propertyTypes = type.GetProperties()
                    .Select(t => t.PropertyType)
                    .ToArray();
                var propertyValues = new List<object>();
                for (var i = 0; i < reader.FieldCount; i++) {
                    propertyValues.Add(reader[i].GetType() == typeof(DBNull) ? null : reader[i]);
                }
                var constructor = type.GetConstructor(propertyTypes);
                var instance = constructor.Invoke(propertyValues.ToArray());

                var rowReadHandler = rowReadHandlers
                    .FirstOrDefault(f => f.HandleType == typeof(T));

                if (rowReadHandler == null) {
                    result.Add((T)instance);
                    Debug.WriteLine("No rowhandler found. Loading to memory collection cache.");
                    continue;
                }

                if (rowReadHandler.Handler(instance)) {
                    Debug.WriteLine("Row handler allowed item.");
                    result.Add((T)instance);
                } else {
                    Debug.WriteLine("Row handler filtered out item.");
                }

            }
        }

    }
}
